# coding = utf-8
import re
import os
from time import sleep

from selenium.webdriver.remote.webdriver import WebDriver as WebDriver_O
from selenium.common import exceptions as SeleniumExceptions
from selenium.webdriver.common.by import By

from qturboframe.lib.utils.CustomError.custom_error import CustomError

from .webelement import WebElement
from ..exceptions import SeleniumError
from ...utils.result import logger
from ...common.__const__ import LOG_PATH


class WebDriver(WebDriver_O):
    """
    Controls a browser by sending commands to a remote server.
    This server is expected to be running the WebDriver wire protocol
    as defined at
    https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol

    :Attributes:
     - session_id - String ID of the browser session started and controlled by this WebDriver.
     - capabilities - Dictionaty of effective capabilities of this browser session as returned
         by the remote server. See https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities
     - command_executor - remote_connection.RemoteConnection object used to execute commands.
     - error_handler - errorhandler.ErrorHandler object used to handle errors.
    """

    def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
                 desired_capabilities=None, browser_profile=None, proxy=None,
                 keep_alive=False, file_detector=None, options=None, is_convert_driver=False, session_id=None):
        if is_convert_driver:
            # 根据传入的driver，创建qturboframe Webdriver
            original_execute = self.execute
            self.execute = self.convert_command_execute
            super().__init__(command_executor, desired_capabilities={})
            self.session_id = session_id
            self.execute = original_execute
        else:
            super().__init__(command_executor
                             , desired_capabilities=desired_capabilities
                             , browser_profile=browser_profile
                             , proxy=proxy
                             , keep_alive=keep_alive
                             , file_detector=file_detector
                             , options=options)

    _web_element_cls = WebElement
    _web_element_stale_flag = True

    def convert_command_execute(self, command, params=None):
        """获取selenium webdriver信息.
        Arguments:
        """
        if command == "newSession":
            # Mock the response
            return {'success': 0, 'value': None, 'sessionId': self.session_id}
        else:
            return self.original_execute(self, command, params)

    def get_alert_text(self,alertoper='accept',timeout=10):
        """
        获取alert弹出框的提示信息

        :Args:
         - alertoper: 对当前对话框的操作，accept为确定，dismiss为取消
         - timeout: 获取弹出框的超时时间，默认为10秒

        :Returns:
            返回值为字符串类型，返回弹出框的提示信息

        :Example:
         - 例1: text = driver.get_alert_text()
         - 例2: text = driver.get_alert_text('dismiss')
         - 例3: driver.get_alert_text('accept', 3)
        """
        times = 0
        alert = None
        while times < timeout:
            try:
                alert = self.switch_to.alert
                break
            except SeleniumExceptions.NoAlertPresentException as e:
                logger.warn(str(e))
                times += 1
                sleep(1)
        if alert:
            alerttext = alert.text
            logger.info("弹出框显示：" + alerttext)
            if alertoper == 'dismiss':
                alert.dismiss()
                logger.debug("弹出框点击取消。")
            else:
                alert.accept()
                logger.debug("弹出框点击确认。")
            return alerttext
        else:
            logger.error('此处没有弹出框！！')
            return None

    def open_new_window(self, url):
        '''
        新开一个窗口，通过执行js来新开一个窗口

        :Args:
         - url: 新窗口访问的url地址

        :Example:
         - 例: driver.open_new_window('https://www.baidu.com')
        '''
        js = 'window.open("{0}");'.format(url)
        self.execute_script(js)
        logger.info('打开新窗口：' + url)

    def switch_to_new_window(self, handles:list):
        '''
        切换到新窗口，返回新窗口的句柄

        :Args:
         - handles: 列表类型，新窗口打开前所有窗口的句柄集合

        :Returns:
            新窗口句柄

        Raises:
            AttributeError: 参数类型不正确，必须是list类型
            SeleniumError: 未产生新窗口，即handles中的句柄均得到匹配

        :Example:
         - 例: handles = driver.window_handles
         driver.open_new_window('https://www.baidu.com')
         driver.switch_to_new_window(handles)

        :Usage:
            先获取浏览器的所有handles，再打开新窗口选项卡，此时使用driver.switch_to_new_window(handles)可以跳转到新打开的选项卡
        '''
        all_handles = self.window_handles
        if not isinstance(handles, list):
            raise AttributeError('handles参数不正确，只能list！！')
        for handle in all_handles:
            if handle not in handles:
                self.switch_to.window(handle)
                curr_title_name = self.title
                logger.info("切换到\'" + curr_title_name + "\'窗口。")
                return handle
        else:
            logger.error('不存在新窗口')
            raise SeleniumError(CustomError.Error_EQ0302301_Window_Not_Exist,'不存在新窗口')

    def switch_to_window(self, window_name, timeout=10):
        """
        切换窗口

        :Args:
         - window_name: int:窗口序号，str:窗口window_name，handle:窗口句柄
         - timeout: 切换窗口的超时时间

        :Raises:
            SeleniumError: 未能切换到新的窗口，新窗口不存在或者切换出错

        :Example:
         - 例1: driver.switch_to_window(1)
         - 例2: driver.switch_to_window('百度一下')
         - 例3: driver.switch_to_window(handle)
        """
        curr_time = 0
        while curr_time < timeout:
            all_handles = self.window_handles
            try:
                if isinstance(window_name, int):
                    self.switch_to.window(all_handles[window_name - 1])
                    logger.info("切换到\'" + all_handles[window_name - 1] + "\'窗口。")
                    return
                else:
                    for handle in all_handles:
                        self.switch_to.window(handle)
                        curr_title_name = self.title
                        if curr_title_name.strip() == window_name.strip() or handle == window_name:
                            logger.info("切换到\'" + curr_title_name + "\'窗口。")
                            return
            except:
                sleep(1)
                curr_time += 1
        logger.error("没有找到window_name为" + str(window_name) + "的网页，请确认!")
        raise SeleniumError(CustomError.Error_EQ0302301_Window_Not_Exist,"没有找到window_name为" + str(window_name) + "的网页，请确认!")

    def get_all_frame(self):
        '''
        获取当前页面中的所有frame、iframe

        :Returns:
            frame\iframe元素的集合

        :Example:
         - 例: frame_list = div.get_all_frame()
        '''
        framelist = self.find_elements_by_tag_name('frame')
        iframelist = self.find_elements_by_tag_name('iframe')
        framelist.extend(iframelist)
        return framelist

    def find_element_by_attribute(self, by, value, assist='', timeout=5, switchframe=False) -> WebElement:
        '''
        根据多属性查找元素，主属性包括by、value两个参数，by对应八种元素查找方式，
        assist指的是辅助属性，即在八种元素查找方式的基础上再加上其他属性条件进行
        高精度的查找；最终结果是一个页面上的可用元素

        :Args:
         - by: 与find_element()的by参数相同
         - value: 与find_element()的value参数相同
         - assist: 辅助属性，'属性名称:属性值'，支持多组属性，多组之间用逗号隔开
         - timeout: 查找元素的超时时间，在timeout时间内会不断的查找直到找到为止，超过timeout时间则不再继续查找
         - switchframe: 自动跳转frame查找元素的开关，默认为False，即不自动跳转frame

        :Returns:
            返回页面上的一个可用元素，WebElement类型

        :Raises:
         - SeleniumError: 未找到元素或者找到多个元素

        :Example:
         - 例: element = driver.find_element_by_attribute(By.CSS_SELECTOR, 'input#su', 'value:百度一下', 3)
        '''
        try:
            elements = self.find_elements_by_attribute(by, value, assist, timeout, switchframe)
            if not elements:
                raise SeleniumError(by + ',' + value + ',' + assist + '属性对象没有找到!')
            lennumber = len(elements)
            if lennumber == 1:
                if not elements[0].is_displayed():
                    try:
                        elements[0].location_once_scrolled_into_view()
                    except:
                        pass
                self._web_element_stale_flag = True
                return elements[0]
            elif lennumber > 0:
                raise SeleniumError(CustomError.Error_EQ03001000_Cannot_Find_Element,by + ',' + value + ',' + assist + '属性对象找到多个，请确认!')
        except SeleniumExceptions.StaleElementReferenceException as msg:
            if self._web_element_stale_flag:
                logger.debug(str(msg) + '\n元素过时，需要重新获取！')
                self._web_element_stale_flag = False
                return self.find_element_by_attribute(by, value, assist, timeout, switchframe)
            else:
                self._web_element_stale_flag = True
                raise SeleniumExceptions.StaleElementReferenceException(str(msg))

    def find_elements_by_attribute(self, by, value, assist='', timeout=5, switchframe=False):
        '''
        根据多属性查找元素，主属性包括by、value两个参数，by对应八种元素查找方式，
        assist指的是辅助属性，即在八种元素查找方式的基础上再加上其他属性条件进行
        高精度的查找；最终结果是符合所有查找条件的元素集合

        :Args:
         - by: 与find_elements()的by参数相同
         - value: 与find_elements()的value参数相同
         - assist: 辅助属性，'属性名称:属性值'，支持多组属性，多组之间用逗号隔开
         - timeout: 查找元素的超时时间，在timeout时间内会不断的查找直到找到为止，超过timeout时间则不再继续查找
         - switchframe: 自动跳转frame查找元素的开关，默认为False，即不自动跳转frame

        :Returns:
            返回页面上的所有符合查找条件的元素集合，[WebElement]类型

        :Raises:
            SeleniumError: 未找到元素

        :Example:
         - 例: ele_list = driver.find_elements_by_attribute(By.CSS_SELECTOR, 'input#su', 'value:百度一下', 3)
        '''
        elements = list()
        timecount = 0
        maxcount = timeout * 10
        while timecount < maxcount:
            times = 0
            while times < 5:
                try:
                    elements = self.find_elements(by, value)
                    # 根据辅助属性进行元素筛选
                    if elements and assist:
                        # 根据辅助属性，判断元素的属性值是否对应，找出筛检对象，
                        assistlist = assist.split(sep=',')
                        for e in assistlist:
                            indexnumber = e.index(':')
                            i = 0
                            # 从主属性对象集合中找出附属属性
                            while i < len(elements):
                                if e[0:indexnumber] == 'text':
                                    temp = e[indexnumber + 1:]
                                    # 因为用正则表达式进行文本匹配，文本可以含有正则表达式的特殊字符，所以把特殊字符进行转义
                                    re_special_character = {'.': '\.'
                                        , '*': '\*'
                                        , '(': '\('
                                        , ')': '\)'
                                        , '$': '\$'
                                        , '+': '\+'
                                        , '[': '\['
                                        , ']': '\]'
                                        , '?': '\?'
                                        , '{': '\{'
                                        , '|': '\|'}
                                    for key, value in re_special_character.items():
                                        temp = temp.replace(key, value)
                                    re_str = '^' + temp + '$'
                                    if e[indexnumber + 1:].startswith('.'):
                                        re_str = re_str.strip('^')
                                    if e[indexnumber + 1:].endswith('.*'):
                                        re_str = re_str.strip('$')
                                    #有时当元素在屏幕上直接不可见时，用text获取到的文本为空
                                    if not (re.search(re_str, elements[i].text.strip()) or re.search(re_str, elements[i].get_attribute("innerText").strip())):
                                        elements.remove(elements[i])
                                    else:
                                        i += 1
                                else:
                                    re_special_character = {'.': '\.'
                                        , '*': '\*'
                                        , '(': '\('
                                        , ')': '\)'
                                        , '$': '\$'
                                        , '+': '\+'
                                        , '[': '\['
                                        , ']': '\]'
                                        , '?': '\?'
                                        , '{': '\{'
                                        , '|': '\|'}
                                    element_attr = elements[i].get_attribute(e[0:indexnumber])
                                    re_word = e[indexnumber + 1:]
                                    for key, value in re_special_character.items():
                                        re_word = re_word.replace(key, value)
                                    # 正则表达式中需要做设定，必须以给定的字符串打头和结尾
                                    if not (element_attr and (
                                            re.search('^' + re_word + '$', element_attr) or re.search(
                                        '^' + re_word + '\s', element_attr) or re.search(
                                        '\s' + re_word + '$', element_attr) or re.search('\s' + re_word + '\s',
                                                                                         element_attr))):
                                        elements.remove(elements[i])
                                    else:
                                        i += 1
                    break
                # 增加异常StaleElementReferenceException抓捕，防止出现对象刷新导致的对象获取失效
                except SeleniumExceptions.StaleElementReferenceException as msg:
                    logger.debug(str(msg) + '\n等待元素刷新！')
                    sleep(0.2)
                    times += 1
                except SeleniumExceptions.NoSuchElementException as msg:
                    logger.debug(str(msg))
                    elements = []
                    break
                except Exception as msg:
                    raise SeleniumError(CustomError.Error_EQ03001000_Cannot_Find_Element,by + ',' + value + '属性对象查找出错：' + str(msg))
            else:
                raise SeleniumError('元素刷新超时！')
            # 进行frame切换查找元素
            if not elements and switchframe:
                framelist = self.get_all_frame()
                for frame in framelist:
                    try:
                        self.switch_to.frame(frame)
                        logger.debug('切换到frame：' + str(frame) + '进行定位。')
                    except SeleniumExceptions.WebDriverException as msg:
                        logger.debug(str(msg))
                        break
                    elements = self.find_elements_by_attribute(by, value, assist, switchframe)
                    if not elements:
                        self.switch_to.parent_frame()
                        logger.debug('返回到上一级frame。')
                    else:
                        break
            timecount = timecount + 1
            if not elements and timecount < maxcount:
                sleep(0.1)
            else:
                return elements
        else:
            return elements

    def exist(self, by, value, assist='', timeout=5, switchframe=False):
        '''
        根据多属性判断元素是否存在，主属性包括by、value两个参数，by对应八种元素查找方式，
        assist指的是辅助属性，即在八种元素查找方式的基础上再加上其他属性条件进行
        高精度的查找；最终结果是一个页面上的可用元素

        :Args:
         - by: 与find_element()的by参数相同
         - value: 与find_element()的value参数相同
         - assist: 辅助属性，'属性名称:属性值'，支持多组属性，多组之间用逗号隔开
         - timeout: 查找元素的超时时间，在timeout时间内会不断的查找直到找到为止，超过timeout时间则不再继续查找
         - switchframe: 自动跳转frame查找元素的开关，默认为False，即不自动跳转frame

        :Returns:
            存在则返回页面上的一个可用元素，WebElement类型；不存在则返回None

        :Example:
         - 例: element = driver.exist(By.CSS_SELECTOR, 'input#su', 'value:百度一下', 3)
        '''
        try:
            findresult = self.find_element_by_attribute(by, value, assist, timeout, switchframe)
        except SeleniumError:
            findresult = None
        return findresult

    def find_first_displayed_element(self, by, value, timeout=5):
        elements = self.find_elements_by_attribute(by, value, timeout=timeout)
        for element in elements:
            try:
                #使元素滚动到可见位置
                element.location_once_scrolled_into_view
            except:
                pass
            if element.is_displayed():
                return element
        return None

    def find_element_contains_text(self,by, value, text, timeout=5):
        elements = self.find_elements(by, value)
        for element in elements:
            try:
                #使元素滚动到可见位置
                element.location_once_scrolled_into_view
            except:
                pass
            if element.is_displayed() and text in element.get_attribute("innerText"):
                return element
        return None

    def find_displayed_elements(self, by, value, timeout=5):
        elements = self.find_elements_by_attribute(by, value, timeout=timeout)
        element_list = []
        for element in elements:
            try:
                #使元素滚动到可见位置
                element.location_once_scrolled_into_view
            except:
                pass
            if element.is_displayed():
                element_list.append(element)
        return element_list

    def get_windows_img(self, imgname):
        '''
        页面截图

        :Args:
         - imgname: 指定截图存放的路径，可以是绝对路径，也可以是相对路径；
             如果是相对路径，则存放在工程目录的logs下，默认在“logs/日期”路径下

        :Returns:
            返回截图位置的绝对路径字符串

        :Example:
         - 例1: img_path = driver.get_windows_img('a.jpg')
         - 例2: img_path = driver.get_windows_img('login/a.jpg')
         - 例3: img_path = driver.get_windows_img('D:/a.jpg')
        '''
        if ':' in imgname:
            screen_name = imgname
        else:
            if LOG_PATH:
                screen_name = os.path.join(LOG_PATH, imgname)
                # 如果日志目录不存在，则创建目录
                if not os.path.exists(LOG_PATH):
                    os.makedirs(LOG_PATH)
            else:
                logger.warn('获取截图根路径失败，无法截图！')
                return None
        try:
            self.get_screenshot_as_file(screen_name)
        except BaseException as e:
            logger.warn(str(e))
            logger.warn("not take screenshot! ignore!!!")
        return screen_name

    def find_element_by_id(self, id_) -> WebElement:
        """Finds an element by id.

        :Args:
         - id\_ - The id of the element to be found.

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_id('foo')
        """
        return WebDriver_O.find_element_by_id(self, id_)

    def find_elements_by_id(self, id_):
        """
        Finds multiple elements by id.

        :Args:
         - id\_ - The id of the elements to be found.

        :Returns:
         - list of WebElement - a list with elements if any was found.  An
           empty list if not

        :Usage:
            elements = driver.find_elements_by_id('foo')
        """
        return WebDriver_O.find_elements_by_id(self, id_)

    def find_element_by_xpath(self, xpath) -> WebElement:
        """
        Finds an element by xpath.

        :Args:
         - xpath - The xpath locator of the element to find.

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_xpath('//div/td[1]')
        """
        return WebDriver_O.find_element_by_xpath(self, xpath)

    def find_elements_by_xpath(self, xpath):
        """
        Finds multiple elements by xpath.

        :Args:
         - xpath - The xpath locator of the elements to be found.

        :Returns:
         - list of WebElement - a list with elements if any was found.  An
           empty list if not

        :Usage:
            elements = driver.find_elements_by_xpath("//div[contains(@class, 'foo')]")
        """
        return WebDriver_O.find_elements_by_xpath(self, xpath)

    def find_element_by_link_text(self, link_text) -> WebElement:
        """
        Finds an element by link text.

        :Args:
         - link_text: The text of the element to be found.

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_link_text('Sign In')
        """
        return WebDriver_O.find_element_by_link_text(self, link_text)

    def find_elements_by_link_text(self, text):
        """
        Finds elements by link text.

        :Args:
         - link_text: The text of the elements to be found.

        :Returns:
         - list of webelement - a list with elements if any was found.  an
           empty list if not

        :Usage:
            elements = driver.find_elements_by_link_text('Sign In')
        """
        return WebDriver_O.find_elements_by_link_text(self, text)

    def find_element_by_partial_link_text(self, link_text) -> WebElement:
        """
        Finds an element by a partial match of its link text.

        :Args:
         - link_text: The text of the element to partially match on.

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_partial_link_text('Sign')
        """
        return WebDriver_O.find_element_by_partial_link_text(self, link_text)

    def find_elements_by_partial_link_text(self, link_text):
        """
        Finds elements by a partial match of their link text.

        :Args:
         - link_text: The text of the element to partial match on.

        :Returns:
         - list of webelement - a list with elements if any was found.  an
           empty list if not

        :Usage:
            elements = driver.find_elements_by_partial_link_text('Sign')
        """
        return WebDriver_O.find_elements_by_partial_link_text(self, link_text)

    def find_element_by_name(self, name) -> WebElement:
        """
        Finds an element by name.

        :Args:
         - name: The name of the element to find.

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_name('foo')
        """
        return WebDriver_O.find_element_by_name(self, name)

    def find_elements_by_name(self, name):
        """
        Finds elements by name.

        :Args:
         - name: The name of the elements to find.

        :Returns:
         - list of webelement - a list with elements if any was found.  an
           empty list if not

        :Usage:
            elements = driver.find_elements_by_name('foo')
        """
        return WebDriver_O.find_elements_by_name(self, name)

    def find_element_by_tag_name(self, name) -> WebElement:
        """
        Finds an element by tag name.

        :Args:
         - name - name of html tag (eg: h1, a, span)

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_tag_name('h1')
        """
        return WebDriver_O.find_element_by_tag_name(self, name)

    def find_elements_by_tag_name(self, name):
        """
        Finds elements by tag name.

        :Args:
         - name - name of html tag (eg: h1, a, span)

        :Returns:
         - list of WebElement - a list with elements if any was found.  An
           empty list if not

        :Usage:
            elements = driver.find_elements_by_tag_name('h1')
        """
        return WebDriver_O.find_elements_by_tag_name(self, name)

    def find_element_by_class_name(self, name) -> WebElement:
        """
        Finds an element by class name.

        :Args:
         - name: The class name of the element to find.

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_class_name('foo')
        """
        return WebDriver_O.find_element_by_class_name(self, name)

    def find_elements_by_class_name(self, name):
        """
        Finds elements by class name.

        :Args:
         - name: The class name of the elements to find.

        :Returns:
         - list of WebElement - a list with elements if any was found.  An
           empty list if not

        :Usage:
            elements = driver.find_elements_by_class_name('foo')
        """
        return WebDriver_O.find_elements_by_class_name(self, name)

    def find_element_by_css_selector(self, css_selector) -> WebElement:
        """
        Finds an element by css selector.

        :Args:
         - css_selector - CSS selector string, ex: 'a.nav#home'

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_css_selector('#foo')
        """
        return WebDriver_O.find_element_by_css_selector(self, css_selector)

    def find_elements_by_css_selector(self, css_selector):
        """
        Finds elements by css selector.

        :Args:
         - css_selector - CSS selector string, ex: 'a.nav#home'

        :Returns:
         - list of WebElement - a list with elements if any was found.  An
           empty list if not

        :Usage:
            elements = driver.find_elements_by_css_selector('.foo')
        """
        return WebDriver_O.find_elements_by_css_selector(self, css_selector)

    def find_element(self, by=By.ID, value=None) -> WebElement:
        """
        'Private' method used by the find_element_by_* methods.

        :Usage:
            Use the corresponding find_element_by_* instead of this.

        :rtype: WebElement
        """

        return WebDriver_O.find_element(self, by, value)

    def find_elements(self, by=By.ID, value=None):
        """
        'Private' method used by the find_elements_by_* methods.

        :Usage:
            Use the corresponding find_elements_by_* instead of this.

        :rtype: list of WebElement
        """

        return WebDriver_O.find_elements(self, by, value)

